Deploy Any Docker App to a VPS with Teploy
Learn how to deploy custom Docker applications to your own VPS using Teploy. Perfect for APIs, background workers, and apps that need full control.
While Teploy automatically detects and builds most applications using Nixpacks, sometimes you need full control. Maybe you're running a custom runtime, have specific system dependencies, or need a multi-stage build process.
That's where Docker comes in. In this tutorial, we'll walk through deploying a custom Docker application to your VPS with Teploy.
What We'll Build
We'll deploy a FastAPI application with:
- A custom Dockerfile
- PostgreSQL database connection
- Background task processing with Celery
- Redis for task queue
This represents a typical production Python application with multiple components.
Prerequisites
Before starting, make sure you have:
- A Teploy account (coming soon)
- A provisioned server (see our getting started guide)
- A GitHub repository with your application
- Basic Docker knowledge
Project Structure
Here's our example project structure:
myapp/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── models.py
│ ├── tasks.py
│ └── database.py
├── Dockerfile
├── docker-compose.yml (for local dev)
├── requirements.txt
└── .env.exampleWriting a Production Dockerfile
A good production Dockerfile should be:
- Small: Use multi-stage builds to reduce image size
- Secure: Run as non-root user
- Cacheable: Order layers by change frequency
Here's our optimized Dockerfile:
# Build stage
FROM python:3.11-slim as builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# Production stage
FROM python:3.11-slim
WORKDIR /app
# Install runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 --gid 1001 appuser
# Copy wheels from builder
COPY --from=builder /app/wheels /wheels
RUN pip install --no-cache /wheels/*
# Copy application
COPY app/ ./app/
# Set ownership
RUN chown -R appuser:appgroup /app
# Switch to non-root user
USER appuser
# Environment variables
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# Expose port
EXPOSE 8000
# Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]Key Points
Multi-stage build: We compile wheels in the builder stage, then copy only the built packages to the production image. This dramatically reduces image size.
Non-root user: Running as root in containers is a security risk. Always create and use a non-root user.
Layer ordering: Static files (apt packages, pip dependencies) come before application code. This maximizes Docker's cache efficiency.
Minimal base image:
python:3.11-slimis much smaller than the full Python image.
Creating the Project in Teploy
- Go to Projects → New Project
- Select GitHub and choose your repository
- Teploy will detect your Dockerfile automatically
- Configure the project:
- Name:
myapp-api - Branch:
main - Server: Your provisioned server
- Name:
Build Settings
When Teploy detects a Dockerfile, it uses it automatically. You can verify or customize in Settings → Build:
Build Type: Dockerfile
Dockerfile Path: ./Dockerfile
Build Context: .Configuring Environment Variables
Your app needs database and Redis connections. In Settings → Environment, add:
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
REDIS_URL=redis://localhost:6379/0
SECRET_KEY=your-secret-key-here
ENVIRONMENT=productionSecurity note: Values marked as secrets are encrypted at rest and never displayed after initial entry.
Deploying the Database
Before deploying our app, we need a PostgreSQL database.
- Go to your project → Services → Add Service
- Select PostgreSQL
- Configure:
- Version: 15
- Database name: myapp
- Username: myapp_user
- Click Deploy
Teploy deploys PostgreSQL as a Docker container on your server. The connection URL is automatically available to your app.
Internal Networking
Databases deployed through Teploy are accessible via Docker's internal network:
postgresql://myapp_user:generated_password@postgres-myapp:5432/myappThis URL isn't exposed to the internet—only containers on the same server can reach it.
Deploying Redis
For our Celery task queue, we need Redis:
- Go to Services → Add Service
- Select Redis
- Configure:
- Version: 7
- Persistence: Enabled (optional, for task durability)
- Click Deploy
Internal URL: redis://redis-myapp:6379/0
Deploying the Main Application
With services ready, deploy your app:
- Go to your project dashboard
- Click Deploy
- Watch the build logs
Teploy will:
- Clone your repository
- Build using your Dockerfile
- Push to our registry
- Deploy with zero downtime
Adding a Celery Worker
Most production apps need background workers. Let's add one for Celery.
- Go to your project → Services → Add Service
- Select Custom Container
- Configure:
- Name: celery-worker
- Image: Uses same image as main app
- Command override:
celery -A app.tasks worker --loglevel=infoThe worker shares the same codebase and environment variables as your main app but runs a different command.
Multiple Workers
For production, you might want specialized workers:
# High-priority worker
Command: celery -A app.tasks worker -Q high_priority --loglevel=info
# Default worker (with concurrency)
Command: celery -A app.tasks worker -Q default --concurrency=4 --loglevel=infoAdd each as a separate service in Teploy.
Adding a Celery Beat Scheduler
For periodic tasks (cron jobs), add Celery Beat:
- Services → Add Service → Custom Container
- Configure:
- Name: celery-beat
- Command:
celery -A app.tasks beat --loglevel=infoImportant: Only run ONE beat scheduler instance. Multiple beats will duplicate scheduled tasks.
Health Checks
Teploy automatically checks if your container is healthy. For more control, add a health check endpoint:
# app/main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
async def health_check():
# Optionally check database connection
return {"status": "healthy"}In Settings → Health Check:
Path: /health
Interval: 30s
Timeout: 10s
Retries: 3If the health check fails after deployment, Teploy automatically rolls back.
Custom Domains and SSL
Add your domain in Settings → Domains:
- Click Add Domain
- Enter
api.yourdomain.com - Configure DNS:
Type: A Name: api Value: [Your server IP] - SSL is provisioned automatically via Let's Encrypt
Persistent Volumes
If your app needs persistent storage (file uploads, for example), configure volumes:
- Settings → Volumes
- Click Add Volume
- Configure:
- Name: uploads
- Mount path: /app/uploads
- Size: 10GB
Volumes persist across deployments and container restarts.
Docker Compose for Local Development
While Teploy handles production, you'll want Docker Compose for local development:
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://myapp:myapp@postgres:5432/myapp
- REDIS_URL=redis://redis:6379/0
depends_on:
- postgres
- redis
postgres:
image: postgres:15
environment:
POSTGRES_USER: myapp
POSTGRES_PASSWORD: myapp
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7
celery:
build: .
command: celery -A app.tasks worker --loglevel=info
environment:
- DATABASE_URL=postgresql://myapp:myapp@postgres:5432/myapp
- REDIS_URL=redis://redis:6379/0
depends_on:
- postgres
- redis
volumes:
postgres_data:Run locally with:
docker-compose up --buildDebugging Deployments
If your deployment fails, check:
Build Logs
The build logs show exactly what happened:
Building...
Step 1/15: FROM python:3.11-slim as builder
Step 2/15: WORKDIR /app
...
ERROR: pip install failedContainer Logs
After deployment, check runtime logs:
2024-01-15 10:30:45 INFO: Application startup complete
2024-01-15 10:30:46 ERROR: Database connection refusedCommon issues:
- Port mismatch: Ensure your app listens on the port Teploy expects (default 8000 for Python)
- Environment variables: Missing or incorrect env vars
- Database connections: Check the connection URL format
Optimizing Build Time
Long builds slow down your iteration. Here are optimization tips:
1. Use .dockerignore
# .dockerignore
.git
.env
__pycache__
*.pyc
.pytest_cache
venv/
node_modules/2. Cache pip downloads
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt3. Order layers strategically
Put rarely-changing layers first (system deps), frequently-changing layers last (app code).
Conclusion
You've deployed a production-ready Docker application with:
- A FastAPI API server
- PostgreSQL database
- Redis cache
- Celery workers for background tasks
- Custom domain with SSL
- Health checks and monitoring
All running on your own VPS, managed through Teploy's dashboard.
The total cost? Your VPS (as low as $5/month) + database storage. Compare that to managed solutions that charge per-request or per-GB.
Next Steps
- Set up preview environments for pull request testing
- Configure auto-scaling for traffic spikes
- Add monitoring with Grafana integration
Questions about Docker deployments? Ask in our contact us or check the documentation.