Introduction
Gitea is a lightweight, self-hosted Git service with features comparable to GitHub, including Git LFS, CI/CD actions, and package registries. This tutorial shows you how to deploy a production-ready Gitea instance on an Onidel VPS in Singapore with enterprise-grade features including PostgreSQL database, MinIO for artifacts and LFS storage, OIDC single sign-on, and dedicated Actions runners.
By the end of this guide, you’ll have a scalable Git platform capable of handling large repositories with LFS support, automated CI/CD workflows, and secure authentication through your existing identity provider.
Prerequisites
Before starting, ensure you have:
- Ubuntu 24.04 LTS VPS with at least 4GB RAM and 40GB storage
- Docker and Docker Compose v2.20+ installed
- Domain name pointing to your server’s IP address
- OIDC provider (optional) for SSO authentication
- S3-compatible storage credentials (for external object storage)
Resource Requirements:
- CPU: 2+ cores (4+ recommended for Actions)
- RAM: 4GB minimum (8GB+ for heavy CI/CD workloads)
- Storage: 40GB+ with fast NVMe for optimal Git operations
Step-by-Step Tutorial
Step 1: Prepare Directory Structure
Create the deployment directory and generate necessary secrets:
mkdir -p ~/gitea-production/{data,postgres,minio,config}
cd ~/gitea-production
# Generate secure passwords
echo "POSTGRES_PASSWORD=$(openssl rand -base64 32)" > .env
echo "MINIO_ROOT_PASSWORD=$(openssl rand -base64 24)" >> .env
echo "GITEA_SECRET_KEY=$(openssl rand -base64 32)" >> .env
echo "INTERNAL_TOKEN=$(openssl rand -base64 32)" >> .env
Step 2: Create Docker Compose Configuration
Create the main docker-compose.yml file with all services:
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: gitea-postgres
restart: unless-stopped
environment:
POSTGRES_DB: gitea
POSTGRES_USER: gitea
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ./postgres:/var/lib/postgresql/data
networks:
- gitea-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U gitea"]
interval: 10s
timeout: 5s
retries: 5
minio:
image: minio/minio:RELEASE.2024-12-13T22-19-12Z
container_name: gitea-minio
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: gitea
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
volumes:
- ./minio:/data
networks:
- gitea-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
gitea:
image: gitea/gitea:1.22-dev
container_name: gitea-server
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
minio:
condition: service_healthy
environment:
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=postgres:5432
- GITEA__database__NAME=gitea
- GITEA__database__USER=gitea
- GITEA__database__PASSWD=${POSTGRES_PASSWORD}
- GITEA__server__DOMAIN=git.yourdomain.com
- GITEA__server__SSH_DOMAIN=git.yourdomain.com
- GITEA__server__ROOT_URL=https://git.yourdomain.com
- GITEA__security__SECRET_KEY=${GITEA_SECRET_KEY}
- GITEA__security__INTERNAL_TOKEN=${INTERNAL_TOKEN}
- GITEA__storage__STORAGE_TYPE=minio
- GITEA__storage__MINIO_ENDPOINT=minio:9000
- GITEA__storage__MINIO_ACCESS_KEY_ID=gitea
- GITEA__storage__MINIO_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}
- GITEA__storage__MINIO_BUCKET=gitea
- GITEA__storage__MINIO_USE_SSL=false
- GITEA__lfs__STORAGE_TYPE=minio
- GITEA__packages__STORAGE_TYPE=minio
- GITEA__actions__ENABLED=true
volumes:
- ./data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:22"
networks:
- gitea-network
gitea-runner:
image: gitea/act_runner:nightly
container_name: gitea-runner
restart: unless-stopped
depends_on:
- gitea
environment:
GITEA_INSTANCE_URL: http://gitea:3000
GITEA_RUNNER_REGISTRATION_TOKEN: ${RUNNER_TOKEN}
volumes:
- ./runner-data:/data
- /var/run/docker.sock:/var/run/docker.sock
networks:
- gitea-network
caddy:
image: caddy:2.7-alpine
container_name: gitea-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./caddy-data:/data
- ./caddy-config:/config
networks:
- gitea-network
networks:
gitea-network:
driver: bridge
volumes:
postgres-data:
minio-data:
gitea-data:
caddy-data:
caddy-config:
Step 3: Configure Reverse Proxy with TLS
Create a Caddyfile for automatic TLS certificate provisioning:
git.yourdomain.com {
reverse_proxy gitea:3000
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}
# Enable HTTP/3
protocols h1 h2 h3
}
# MinIO admin console (optional)
minio-admin.yourdomain.com {
reverse_proxy minio:9001
}
Step 4: Initialize MinIO Buckets
Start MinIO first to create the required buckets:
# Start only MinIO initially
docker-compose up -d minio
# Install MinIO client
wget https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc && sudo mv mc /usr/local/bin/
# Configure MinIO client
mc alias set gitea-minio http://localhost:9000 gitea $(grep MINIO_ROOT_PASSWORD .env | cut -d'=' -f2)
# Create required buckets
mc mb gitea-minio/gitea
mc mb gitea-minio/gitea-lfs
mc mb gitea-minio/gitea-packages
Step 5: Deploy Complete Stack
Start all services and verify deployment:
# Deploy the complete stack
docker-compose up -d
# Monitor logs for any issues
docker-compose logs -f gitea
# Check service health
docker-compose ps
Warning: Initial startup may take 2-3 minutes as Gitea initializes the database schema and connects to object storage.
Step 6: Configure OIDC SSO
Navigate to your Gitea admin panel at https://git.yourdomain.com and configure OIDC authentication:
- Go to Site Administration → Authentication Sources
- Add new OAuth2 provider with these settings:
- Provider: OpenID Connect
- Client ID: Your OIDC client ID
- Client Secret: Your OIDC client secret
- Discovery URL:
https://your-oidc-provider/.well-known/openid-configuration
Step 7: Register Actions Runner
Register the Actions runner with your Gitea instance:
# Get runner registration token from Gitea admin panel
# Add it to your .env file
echo "RUNNER_TOKEN=your_registration_token_here" >> .env
# Restart the runner service
docker-compose restart gitea-runner
# Verify runner registration
docker-compose logs gitea-runner
Best Practices
Security Hardening
- Enable 2FA: Force two-factor authentication for all administrators
- Restrict SSH access: Use non-standard port 2222 and implement key-based authentication only
- Network isolation: Keep database and object storage on internal network
- Regular updates: Implement automated security updates for the host system
Backup Strategy
Implement comprehensive backups for all data layers:
# Backup PostgreSQL database
docker-compose exec postgres pg_dump -U gitea gitea > backup-$(date +%Y%m%d).sql
# Backup Git repositories
tar -czf repos-backup-$(date +%Y%m%d).tar.gz ./data/git/repositories/
# Sync MinIO data to external S3
mc mirror gitea-minio/gitea s3/your-backup-bucket/gitea-$(date +%Y%m%d)/
Consider implementing automated backups using encrypted VPS backup solutions for comprehensive protection.
Performance Optimization
- Database tuning: Configure PostgreSQL shared_buffers and work_mem for your workload
- Git LFS caching: Enable MinIO caching for frequently accessed LFS objects
- Actions concurrency: Scale runner instances based on CI/CD demand
- Monitoring: Implement observability stack for performance insights
Scalability Considerations
For high-traffic environments:
- Database clustering: Consider PostgreSQL high availability setup
- Object storage: Use distributed MinIO cluster for large-scale artifact storage
- Load balancing: Deploy multiple Gitea instances behind enterprise load balancers
Conclusion
You now have a production-ready Gitea deployment with enterprise features including PostgreSQL persistence, S3-compatible LFS storage, OIDC authentication, and automated CI/CD capabilities. This setup provides a solid foundation for teams requiring self-hosted Git services with GitHub-like functionality.
The containerized architecture ensures easy maintenance and scaling, while the comprehensive backup strategy protects your critical source code assets. For enhanced security and global accessibility, consider deploying this stack on an Onidel VPS in Singapore with our high-performance EPYC processors and enterprise-grade NVMe storage.
We encourage you to explore Onidel’s VPS offerings in Singapore, Sydney, and Amsterdam for hosting your critical development infrastructure with features like automatic backups, private networks, and advanced security options including AMD-SEV encryption.




