
Deploy Django on a VPS with Docker: Step-by-Step Guide
Lukas MauserDeploying a Django application on a Virtual Private Server (VPS) is a cheap, privacy friendly and flexible way to host your projects. In this guide, we'll walk you through the process of deploying a Django app on a VPS using Docker. We'll cover everything from setting up your Django app for Docker, choosing a VPS provider, configuring the server, and deploying the app.
Who is this guide for?
This guide is for you if you:
- are curious about self-hosting and want to learn some fundamentals
- want to run small projects or side projects
- want to save money on hosting costs
This guide is not for you if you:
- are looking for the absolute fastest way to get started
- want to avoid ongoing maintenance
- need to run mission-critical workloads and don't know what you're doing
For a simple, managed way to get started, check out our product: sliplane.io
Prerequisites
- A Django application, ready to be deployed
- An account on Hetzner (or other your preferred VPS provider)
- A domain name
In a nutshell
We will use Docker to containerize our Django app and Postgres as a database. Then, we'll choose a VPS provider, set up the server, and deploy the app using Docker Compose and a reverse proxy (Caddy) for SSL termination.
Dockerize the Django app
Why Docker?
Docker makes it easy to run custom software in the cloud. You can define everything your Django app needs in a Dockerfile and then spin up the app in an isolated container with all dependencies installed.
For example, our Django app requires a specific version of Python and a web server (like Gunicorn) to handle requests. We can define all of that in a Dockerfile and then build an image from it. Once the image is built, we can run it on our server.
We can also run a Postgres database in another container and connect our Django app to it without running into dependency conflicts. Docker gives you access to a huge ecosystem of prebuilt images for databases, caches, web servers, and much more.
Setting up Docker for Django
If you want a detailed introduction, check out my Django in Docker tutorial.
TL;DR: Create two files named Dockerfile and docker-compose.yml in the root of your Django project and add the following content:
Dockerfile:
# Stage 1: Base build stage
FROM python:3.13-slim AS builder
# Create the app directory
RUN mkdir /app
# Set the working directory
WORKDIR /app
# Set environment variables to optimize Python
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Upgrade pip and install dependencies
RUN pip install --upgrade pip
# Copy the requirements file first (better caching)
COPY requirements.txt /app/
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Stage 2: Production stage
FROM python:3.13-slim
RUN useradd -m -r appuser && \
mkdir /app && \
chown -R appuser /app
# Copy the Python dependencies from the builder stage
COPY --from=builder /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/
COPY --from=builder /usr/local/bin/ /usr/local/bin/
# Set the working directory
WORKDIR /app
# Copy application code
COPY --chown=appuser:appuser . .
# Set environment variables to optimize Python
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Switch to non-root user
USER appuser
# Expose the application port
EXPOSE 8000
# Start the application using Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "myproject.wsgi:application"]
docker-compose.yml:
services:
web:
build: .
ports:
- "8000:8000"
env_file:
- .env
depends_on:
- db
db:
image: postgres:17
volumes:
- postgres_data:/var/lib/postgresql/data
env_file:
- .env
volumes:
postgres_data:
To test locally, make sure Docker is installed and running. Install Docker. You can test locally if everything works by running:
docker compose up
This spins up the django app and a postgres database. You can access the django app at http://localhost:8000. Before you deploy your Django app, make sure everything works locally.
Build and push the Django Docker image
The compose file above is great for local development, but not ideal for production, because it requires the server to build the image every time a new version is deployed. In production, you want to prebuild your Django app and store it in a container registry, so your server can simply pull the image and run it (just like the Postgres container).
You can build your image using this command:
docker build -t my-django-app .
After the image is build, we will push it to a container registry - a warehouse for Docker images.
Push to a container registry: I recommend GitHub Container Registry (GHCR) because it's free for private images. To get started, create a personal access token in GitHub first.
Make sure the token has write:packages, read:packages, and delete:packages scopes. See GitHub's docs for details.
You can then use this token to authenticate which is required to push images to your private workspace:
Authenticate with GHCR:
echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin
Tag and push your image:
docker tag my-django-app ghcr.io/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME:latest
docker push ghcr.io/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME:latest
Here we use the latest tag for simplicity, but you can use semantic versioning or git commit hashes for better version control and rollbacks.
Every time you make changes to your Django app, rebuild and push the image. You might see how this can get tedious with frequent deployments, which is why these steps are often automated in a CI/CD pipeline. I won't go into the details in this tutorial, but I created a seperate post about how to create a CI/CD pipeline with GitHub Actions.
When the image is pushed, it's time to set up the server.
Choosing a VPS
1. What VPS to choose?
We compared some popular providers in this post on the top 5 cheap VPS providers and have had great experiences with Hetzner. They're affordable, reliable, and we've deployed thousands of servers with them. If you use our Hetzner affiliate link, you'll get €20 in credit.
2. How big does my VPS need to be?
Start with the smallest option available and scale up as needed. You'd be surprised how far a basic VPS can get you — a Postgres database and a small Django app can run on as little as 1GB of RAM. You can always resize your server later. Start simple: run both the database and Django app on the same server (see our guide on running multiple apps on a single VPS). Only worry about more complex setups when you actually need to scale.
3. Where do I want my VPS to be located? Choose a location close to your users for best performance. If latency isn't a big concern, pick the cheapest location. Prices vary due to electricity, taxes, etc. Hetzner, for example, has data centers in Germany and Finland that are usually cheaper than other locations.
4. What processor architecture - x86 or arm?
Arm is getting more popular because it's cheaper, but we've run into availability issues and some software compatibility problems. We usually stick with x86. If you develop on x86 and deploy on arm, you might hit unexpected issues. The good thing about VPSs: you only pay for the time your server runs, so you can test an arm server for a few cents and switch back to x86 if needed.
5. Shared or dedicated?
Start with shared and upgrade if necessary. Shared is generally slower and can sometimes be unpredictable, so provider choice matters. With Hetzner, we've had good experiences and shared server performance has been consistent and reliable.
6. What OS to choose? Linux is the way to go for servers: it's free, secure, and reliable. The most popular distributions for servers are Ubuntu, Debian, CentOS, Fedora, and Arch Linux.
I use Ubuntu because that's what I learned on—use the latest LTS version available. Differences between distributions are mostly a matter of preference. If you're unsure, go with Ubuntu. If you have specific requirements, you'll know what to do.
In general, stick with what's popular so you can find help online when needed.
Setting up the server
Connect to your server
After your server is set up and running, connect to it via SSH. By default, most providers give you a root user and password, but you can often provide your SSH public key during setup for more security.
Open a terminal on your local machine and run:
ssh root@your-server-ip
Replace your-server-ip with the IP address of your server. You can usually find it in your VPS provider's dashboard or in a welcome email.
If you're new to SSH, check out this guide from github about how you can generate ssh keys and add them to your agent.
Hardening the server
Hardening means making it harder for attackers to compromise your server. This should be the first thing you do after setup. Measures include disabling root login, using SSH keys instead of passwords, setting up a firewall, keeping your system updated, using fail2ban, etc. It's good practice to use a hardening script so you don't forget anything important. I don't want to provide an outdated script here, but you can find plenty of good hardening scripts online or write your own. If you use a script from the internet, make sure you understand what it does and trust the source.
Firewall
Hetzner offers a simple firewall you can configure via their dashboard. Only allow traffic on ports 22 (SSH), 80 (HTTP), and 443 (HTTPS).
It's recommended to use the Hetzner firewall in addition to a local firewall like UFW or iptables, since it blocks traffic before it reaches your server. This saves resources and adds an extra layer of protection (especially since Docker can sometimes bypass local firewalls).
Install Docker
Follow the official Docker install guide to install Docker on your server.
Now we're ready to deploy our Django app.
Run the django app on your server
We'll use Docker Compose again to run both the Django app and the Postgres database. However, we need to modify the compose file slightly for production.
First, set up your environment variables. You can use Docker secrets or .env files to store sensitive data like database passwords. For a simple setup, create a .env file in the project directory on the server:
POSTGRES_DB=mydb
POSTGRES_USER=myuser
POSTGRES_PASSWORD=mypassword
DATABASE_URL=postgres://myuser:mypassword@db:5432/mydb
DJANGO_SECRET_KEY=your-secret-key
If you're a bit more serious about security, consider using Docker secrets instead.
Here are 2 changes we need to make to the docker-compose.yml for production:
- Change the
webservice to pull the prebuilt image from your container registry instead of building from source. We'll also introduce a third service: a reverse proxy. - Add a reverse proxy service. The reverse proxy is necessary in production, because it handles SSL termination and forwards requests to your Django app running in Docker. Most reverse proxies can also do some other cool tricks, like caching requests, compressing responses, and more. I compared 5 different reverse proxies in this post. One of the simplest is Caddy, which comes with automatic SSL via Let's Encrypt and is very easy to configure.
Your final docker-compose.yml will look like this:
services:
web:
image: ghcr.io/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME:latest
ports:
- "8000:8000"
env_file:
- .env
depends_on:
- db
db:
image: postgres:17
volumes:
- postgres_data:/var/lib/postgresql/data
env_file:
- .env
caddy:
image: caddy:2.10.2
restart: unless-stopped
cap_add:
- NET_ADMIN
ports:
- "80:80"
- "443:443"
volumes:
- $PWD/conf:/etc/caddy
- caddy_data:/data
- caddy_config:/config
To configure Caddy, create a conf directory in the same location as your docker-compose.yml file and add a Caddyfile inside it:
yourdomain.com {
reverse_proxy web:8000
}
This will make sure Caddy forwards all requests from your domain to the Django app running on port 8000. It's important to provide your actual domain name here, as Caddy uses it to obtain SSL certificates automatically. Use A/AAAA records to point your domain to your server's IP address so requests reach your server.
Once your domain is setup and points to your server, you can spin up the services with:
docker compose up -d
That's it! Your Django app should now be running on your VPS.
To deploy a new version of your app, pull the new image and restart the web service:
docker compose pull web
docker compose up -d web
As mentioned before, you can automate deployments with a CI/CD pipeline.
Summary
Deploying Django on a VPS is cheap and a great way to learn about infrasturcture.
What we used in this guide:
- Docker to containerize the Django app
- Postgres as the database
- Caddy as a reverse proxy for SSL termination
- Hetzner as the VPS provider
- GitHub Container Registry to store the Docker image
There are endless ways to improve this setup: add backups, monitoring, logging, scaling, and more. When you start your self-hosting journey, prepare to learn a lot! If you want to save yourself some hassle, check out our product: sliplane.io, which comes with all of this out of the box. It's a middle ground that gives you the best of self-hosting without the pain of ongoing server maintenance.