Skip to main content

Dockerize A Fullstack App on VPS

· 3 min read
Femi Adigun
Senior Software Engineer & Coach

Deploy A Fullstack App to VPS with Docker

update your droplet

sudo apt update && sudo apt upgrade -y

Pull the frontend and backend code into the /opt directory i.e /opt/backend and /opt/frontend

Install Docker and certbot


sudo apt-get update

# Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh

# Docker Compose
apt-get update
apt-get install docker-compose-plugin
or
sudo apt install -y docker-compose

# Certbot
apt-get install certbot

verify docker

docker --version
docker-compose --version

Map A record to your domain's DNS

Enable Docker to run on boot

sudo systemctl enable --now docker

Get SSL ertificates before running docker containers.

# Stop any running web servers first
systemctl stop nginx # if nginx is running

# Get certs for both domains
certbot certonly --standalone -d yourdomain.com
certbot certonly --standalone -d api.yourdomain.com

Set up backend


cd /opt/backend

git pull origin main

# Back on the droplet:
# Create/edit .env file
nano .env
# Add your environment variables:
POSTGRES_USER=youruser
POSTGRES_PASSWORD=yourpassword
POSTGRES_DB=yourdb
DOMAIN=yourdomain.com
CORS_ORIGINS=https://yourdomain.com

# Start the backend services
docker-compose up -d

Here is the Dockerfile for the backend application


FROM python:3.11.1-slim

# Set env vars
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

#install deps
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

# install python deps
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# copy fastapi code
COPY . .

# Expose port
EXPOSE 8000

# set entry point
CMD ["uvicorn", "api.app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Here is the Docker compose file for the database, environment variables and environment management. Make sure the environment variables are corectly captured in .env file


version: "3"

services:
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- /etc/letsencrypt:/etc/letsencrypt
depends_on:
- frontend
- app

frontend:
build: ../frontend # Adjust this path to your frontend directory
environment:
- NEXT_PUBLIC_API_URL=https://api.${DOMAIN}
- NEXT_PUBLIC_DOMAIN=${DOMAIN}
depends_on:
- app

db:
env_file: .env
image: postgres
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- db-data:/var/lib/postgresql/data

app:
build: .
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
DOMAIN: ${DOMAIN}
depends_on:
- db

volumes:
db-data:


Here is the NGINX for SSL and request routing.


server {
listen 80;
server_name ${DOMAIN};
return 301 https://$server_name$request_uri;
}

server {
listen 443 ssl;
server_name ${DOMAIN};

ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;

# Frontend
location / {
proxy_pass http://frontend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}

# Backend API
location /api {
proxy_pass http://app:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

Create deployment script

#!/bin/bash
# deploy.sh

# Pull latest changes
git pull

# Build and restart containers
docker-compose down
docker-compose build
docker-compose up -d

# Check logs
docker-compose logs -f

Create SSL renewal script

# Create renewal script
echo "#!/bin/bash
certbot renew
docker-compose restart nginx" > /root/renew-certs.sh

chmod +x /root/renew-certs.sh

# Add to crontab
(crontab -l 2>/dev/null; echo "0 0 1 * * /root/renew-certs.sh") | crontab -