Deployment Guide for Django + Vue.js project on VPS

Table of contents
- Overview
- Prerequisites
- 1. VPS Setup
- 2. System Dependencies
- 3. Database: PostgreSQL
- 4. Redis for Celery
- 5. Codebase Deployment
- 6. Python Virtual Environment
- 7. Frontend Build (Vue.js)
- 8. Django Migrations & Static Files
- 9. Application Server: Gunicorn
- 10. Background Workers: Celery & Beat
- 11. Reverse Proxy: Nginx
- 12. SSL/TLS with Let's Encrypt
- 13. Monitoring & Logging
- 14. Automated Backups
- 15. Routine Maintenance
- 16. Nginx permissions
- Few helpful commands to restart services after deployment
- 1. Reload all systemd unit files (after any edits)
- 2. Restart your Django app (Gunicorn)
- 3. Restart Celery worker & beat
- 4. Restart Nginx (reverse proxy & static files)
- 5. (Optional) Restart backing services
- 6. (Optional) I made a script to restart all services at once. Feel free to tweak it as it fits your needs.

Overview
This guide provides a secure, production-ready deployment process for the Focus Timer application on a Linux VPS. It follows best practices at each step and includes brief explanations to help beginners understand.
Prerequisites
- A fresh Ubuntu/Debian VPS (18.04+ or 20.04+).
- Root or sudo access.
- A domain name pointing to your VPS.
- Basic familiarity with Linux shell.
1. VPS Setup
1.1 Create Unprivileged User
Why? Running services as non-root improves security.
# Add a user, no password login, add to sudo group
adduser --disabled-password --gecos "" focususer
usermod -aG sudo focususer
# Set up SSH access for the new user
# Copy root's authorized keys to the new user
mkdir -p /home/focususer/.ssh
cp /root/.ssh/authorized_keys /home/focususer/.ssh/authorized_keys
# Set ownership and permissions
chown -R focususer:focususer /home/focususer/.ssh
chmod 700 /home/focususer/.ssh
chmod 600 /home/focususer/.ssh/authorized_keys
Explanation:
adduser
: Creates a new Linux user with default settings.--disabled-password
: Prevents setting a login password (forces SSH key auth only).--gecos ""
: Supplies empty fields for the user's full name and contact info.usermod -aG sudo
: Appends (-a
) the user to thesudo
group, allowing administrative commands.
1.2 Secure SSH Access
Why? Prevent unauthorized root or password-based logins.
# On your local machine, generate SSH key if needed:
ssh-keygen -t ed25519 -C "your_email@example.com"
# Copy public key to server:
ssh-copy-id focususer@your.domain.com
On server, edit /etc/ssh/sshd_config:
Port 22
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM yes
sudo systemctl reload ssh
Explanation:
ssh-keygen -t ed25519
: Generates a modern, high-security Ed25519 key pair.-C "comment"
: Adds an identifying comment (often your email).ssh-copy-id
: Installs your public key on the remote server's~/.ssh/authorized_keys
file, allowing key-based logins.PermitRootLogin no
: Disables SSH login asroot
, forcing administrative access via a less-privileged user plussudo
.PasswordAuthentication no
: Turns off password-based logins entirely; only key-based auth is allowed.ChallengeResponseAuthentication no
: Disables keyboard-interactive (challenge-response) methods, such as one-time passwords or other PAM-driven prompts, preventing any fallback from key-based authentication.- Without this setting, SSH could invoke PAM's challenge modules (e.g. OTP or custom scripts) that might allow weaker or interactive authentication paths.
UsePAM yes
: Enables Pluggable Authentication Modules (PAM) integration.- Even with password logins disabled, PAM handles account and session management after a successful key login.
- PAM modules enforce additional security policies (e.g. account expiration, lockouts, resource limits, logging).
- Ensures that system-wide policies (fail2ban, pam_tally2, custom modules) can apply to every SSH session.
systemctl reload ssh
: Applies the above SSH daemon changes without dropping existing connections.
1.2.1. Logging in as focususer
Why? Operating as a non-root user mitigates the risk of accidental system-wide changes.
# Exit the current root SSH session:
exit
# From your local machine, SSH back in as the unprivileged user:
ssh focususer@your.domain.com
Explanation:
exit
: Ends the current SSH session as root and returns you to your local shell.ssh focususer@your.domain.com
: Initiates a new SSH session using your key for thefocususer
account.
1.3 Firewall & Fail2ban
Why? Limit open ports and block brute-force attempts.
# Install UFW & Fail2ban
sudo apt update && sudo apt install -y ufw fail2ban
# Basic UFW rules
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
# Configure Fail2ban (e.g., /etc/fail2ban/jail.local)
sudo tee /etc/fail2ban/jail.local <<EOF
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
[sshd]
enabled = true
port = 22
filter = sshd
logpath = /var/log/auth.log
EOF
sudo systemctl restart fail2ban
Explanation:
ufw default deny incoming
: Blocks all incoming traffic by default.ufw default allow outgoing
: Allows all outbound traffic.ufw allow OpenSSH
: Opens SSH port (usually 22).ufw allow 'Nginx Full'
: Opens HTTP (80) and HTTPS (443) for web traffic.ufw enable
: Activates the firewall with the defined rules.fail2ban
: Monitors log files and bans IPs after too many failed login attempts.- In
jail.local
:bantime
: Duration (1h) to ban offenders.findtime
: Time window (10m) to track failures.maxretry
: Max allowed failures (5) before ban.
1.4 Automatic Security Updates
Why? Keep critical packages patched.
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades
Explanation:
unattended-upgrades
: Automatically downloads and installs security updates.dpkg-reconfigure unattended-upgrades
: Opens a configuration prompt to enable auto-updates.
2. System Dependencies
Install tools required for building and deployment.
sudo apt install -y git build-essential curl libpq-dev
Explanation:
git
: Version control system to clone your repository.build-essential
: Installs GCC, make, and other build tools for compiling native extensions.curl
: Tool to transfer data from or to a server (used for downloading scripts).libpq-dev
: Development headers for PostgreSQL, required by psycopg2 Python package.
3. Database: PostgreSQL
sudo apt install -y postgresql postgresql-contrib
# Create database and user
check ./setup.md for PostgreSQL setup
# Secure PostgreSQL: only listen locally and enforce password auth
sudo sed -i "s/#listen_addresses = 'localhost'/listen_addresses = 'localhost'/" /etc/postgresql/*/main/postgresql.conf
# Only allow local socket and password auth for TCP
host all all 127.0.0.1/32 md5
sudo systemctl restart postgresql
Brief: PostgreSQL is reliable and scalable for production.
4. Redis for Celery
sudo apt install -y redis-server
sudo systemctl enable --now redis-server.service
# Secure Redis: bind only to localhost, enable protected mode, optional password
sudo sed -i "s/^bind .*/bind 127.0.0.1 ::1/" /etc/redis/redis.conf
sudo sed -i "s/^protected-mode no/protected-mode yes/" /etc/redis/redis.conf
sudo sed -i "s/# requirepass foobared/requirepass StrongRedisPassw0rd/" /etc/redis/redis.conf
sudo systemctl restart redis
Brief: Redis acts as broker and result backend for Celery.
5. Codebase Deployment
5.1 Clone Repository
# Switch to DIR where you want to keep your project
# ( using a sample project name, replace with your actual repo )
git clone https://github.com/your-repo/focus-timer-django-vue.git
cd focus-timer-django-vue
5.2 Environment Variables
Why? Keep secrets out of source code.
# In project root, create .env
touch .env
# Populate .env with necessary variables
# Secure .env file permissions
chmod 600 .env
6. Python Virtual Environment
cd backend
python3 -m venv .venv
source .venv/bin/activate
pip install uv
pip install -r requirements.txt
7. Frontend Build (Vue.js)
cd ../frontend-vue
# install node
curl -fsSL https://raw.githubusercontent.com/mklement0/n-install/stable/bin/n-install | bash -s 22
npm run build
Brief: Outputs static assets in dist/
.
8. Django Migrations & Static Files
cd ../backend
source .venv/bin/activate
python manage.py migrate
python manage.py collectstatic --noinput
9. Application Server: Gunicorn
pip install gunicorn
Create /etc/systemd/system/gunicorn.service
:
[Unit]
Description=Gunicorn daemon for Focus Timer
After=network.target
[Service]
User=focususer
Group=www-data
WorkingDirectory=/home/focususer/focus-timer-django-vue/backend
ExecStart=/home/focususer/focus-timer-django-vue/.venv/bin/gunicorn \
backend.wsgi:application \
--bind 127.0.0.1:8000 \
--workers 4
EnvironmentFile=/home/focususer/focus-timer-django-vue/.env
Restart=on-failure
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now gunicorn
10. Background Workers: Celery & Beat
- Create
/etc/systemd/system/celery.service
:
[Unit]
Description=Celery worker service for Focus Timer
After=network.target
[Service]
Type=simple
User=focususer
Group=www-data
WorkingDirectory=/home/focususer/focus-timer-django-vue/backend
EnvironmentFile=/home/focususer/focus-timer-django-vue/.env
ExecStart=/home/focususer/focus-timer-django-vue/.venv/bin/celery -A backend worker \
--loglevel=info
Restart=on-failure
[Install]
WantedBy=multi-user.target
- Create
/etc/systemd/system/celery-beat.service
:
[Unit]
Description=Celery Beat scheduler for Focus Timer
After=network.target
[Service]
Type=simple
User=focususer
Group=www-data
WorkingDirectory=/home/focususer/focus-timer-django-vue/backend
EnvironmentFile=/home/focususer/focus-timer-django-vue/.env
ExecStart=/home/focususer/focus-timer-django-vue/.venv/bin/celery -A backend beat \
--loglevel=info \
--scheduler django_celery_beat.schedulers:DatabaseScheduler
Restart=on-failure
[Install]
WantedBy=multi-user.target
- Enable & start both services:
Explanation:sudo systemctl daemon-reload sudo systemctl enable --now celery celery-beat
Type=forking/simple
: ensures proper startup and process tracking.--detach
: runs worker/beat in background.Restart=on-failure
: automatically recovers from crashes.
11. Reverse Proxy: Nginx
Install and configure /etc/nginx/sites-available/focus-timer
:
server {
listen 80;
server_name tymr.online www.tymr.online;
root /home/focususer/focus-timer-django-vue/frontend-vue/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /static/ {
alias /home/focususer/focus-timer-django-vue/backend/static/;
}
location /api/ { proxy_pass http://127.0.0.1:8000/api/; include proxy_params; }
location /auth/ { proxy_pass http://127.0.0.1:8000/auth/; include proxy_params; }
location /admin/ { proxy_pass http://127.0.0.1:8000/admin/; include proxy_params; }
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
client_max_body_size 10M;
}
Enable and reload:
sudo ln -s /etc/nginx/sites-available/focus-timer /etc/nginx/sites-enabled/
sudo nginx -t
sudo ln -s /etc/nginx/sites-available/focus-timer-redirect /etc/nginx/sites-enabled/
sudo systemctl reload nginx
Explanation:
- Security headers prevent clickjacking, MIME-sniffing, enforce HTTPS.
client_max_body_size
: limits upload size to mitigate DoS.- HTTP->HTTPS redirect ensures all traffic is encrypted.
12. SSL/TLS with Let's Encrypt
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d tymr.online -d www.tymr.online
Brief: Provides free, auto-renewing certificates.
13. Monitoring & Logging
- Use
journalctl -u gunicorn -f
andjournalctl -u celery -f
. - Set up Logrotate for Gunicorn logs if needed.
- Consider external monitoring (Prometheus/Grafana, UptimeRobot).
14. Automated Backups
- Write a cron job for nightly PostgreSQL dumps:
sudo crontab -u focususer -e # Add: 0 2 * * * pg_dump -U focusdbuser focusdb | gzip > ~/backups/db-$(date +\%F).sql.gz
- Secure backup storage (offsite or S3).
15. Routine Maintenance
- Keep OS & packages updated:
sudo apt update && sudo apt upgrade -y
- Review logs, monitor disk and memory.
16. Nginx permissions
Why? Nginx needs access to static files and directories.
# Ensure Nginx can read static files
sudo chmod o+x /home/focususer
sudo chmod -R o+rX /home/focususer/focus-timer-django-vue/frontend-vue/dist
sudo chmod -R o+rX /home/focususer/focus-timer-django-vue/backend/static
sudo chmod -R o+rX /home/focususer/focus-timer-django-vue/backend/media
sudo chmod -R o+x /home/focususer/focus-timer-django-vue/frontend-vue
chmod +x /home/focususer/focus-timer-django-vue/.venv/bin/gunicorn
chmod +x /home/focususer/focus-timer-django-vue/.venv/bin/celery
chmod u+x restart_all.sh
Few helpful commands to restart services after deployment
1. Reload all systemd unit files (after any edits)
sudo systemctl daemon-reload
2. Restart your Django app (Gunicorn)
sudo systemctl restart gunicorn sudo systemctl status gunicorn # check exit status immediately journalctl -u gunicorn -f # live logs
3. Restart Celery worker & beat
sudo systemctl restart celery celery-beat sudo systemctl status celery # check worker status sudo systemctl status celery-beat journalctl -u celery -f # live worker logs journalctl -u celery-beat -f # live beat logs
4. Restart Nginx (reverse proxy & static files)
sudo systemctl restart nginx sudo systemctl status nginx sudo journalctl -u nginx -f
5. (Optional) Restart backing services
sudo systemctl restart redis-server postgresql sudo systemctl status redis-server postgresql sudo journalctl -u redis-server -f sudo journalctl -u postgresql -f
6. (Optional) I made a script to restart all services at once. Feel free to tweak it as it fits your needs.
#!/usr/bin/env bash
# Script to restart all Focus Timer services
set -euo pipefail
# Parse options
SKIP_FRONTEND=false
for arg in "$@"; do
case $arg in
--skip-frontend)
SKIP_FRONTEND=true
;;
esac
done
echo "Installing backend requirements..."
.venv/bin/uv pip install -r backend/requirements.txt
echo "Applying database migrations..."
.venv/bin/python backend/manage.py migrate --noinput
echo "Collecting static files..."
.venv/bin/python backend/manage.py collectstatic --noinput
if [ "$SKIP_FRONTEND" = false ]; then
echo "Building frontend..."
( cd frontend-vue && npm ci && npm run build )
else
echo "Skipping frontend build due to --skip-frontend option"
fi
echo "Reloading systemd daemon..."
sudo systemctl daemon-reload
services=(
gunicorn
celery
celery-beat
nginx
redis-server
postgresql
)
for service in "${services[@]}"; do
echo "Restarting $service..."
sudo systemctl restart "$service"
done
echo "Waiting for services to settle..."
sleep 2
echo "Services status:"
for service in "${services[@]}"; do
echo "===== $service ====="
sudo systemctl status "$service" --no-pager
done
echo "All services restarted."
Save this script as deploy.sh
and make it executable:
chmod +x deploy.sh
Subscribe to my newsletter
Read articles from Saurav Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Saurav Sharma
Saurav Sharma
I am a Self Taught Backend developer With 3 Years of Experience. Currently, I am working at a tech Startup based in The Bahamas. Here are my skills so far - 💪Expert at - 🔹Python 🔹Django 🔹Django REST framework 🔹Celery ( for distributed tasks ) 🔹ORM ( Know how to write fast queries & design models ) 🔹Django 3rd party packages along with postgresQL and mysql as Databases. 🔹Cache using Redis & Memcache 🔹Numpy + OpenCV for Image Processing 🔹ElasticSearch + HayStack 🔹Linux ( Debian ) 😎 Working Knowledge - Html, CSS, JavaScript, Ajax, Jquery, Git ( GitHub & BitBucket ), Basic React & React Native, Linux ( Arch ), MongoDB, VPS 🤠 Currently Learning - 🔹More Deep Dive into Django 🔹Docker 🔹Making APIs more Robust 🔹NeoVim 🔹System Design ☺️ Will Be Learn in upcoming months - 🔹GraphQL 🔹 Rust language Other than above, there is not a single technology ever exists that i can't master if needed.