Kompletní návod pro nasazení CITYA aplikace (Backend + Dispatcher) v lokálním prostředí.
gcloud CLI (pro migraci dat z GCP)psql klient (pro práci s databází)citya-development nebo citya-productioncurl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
# Odhlásit se a znovu přihlásit
curl https://sdk.cloud.google.com | bash
exec -l $SHELL
gcloud init
gcloud auth application-default login
Vytvořte adresářovou strukturu:
mkdir -p ~/projects/citya-local/{repos,docker,backups}
cd ~/projects/citya-local
Výsledná struktura:
~/projects/citya-local/
├── repos/
│ ├── backend/ # .NET Backend API
│ └── dispatcher/ # Vue.js/Quasar Frontend
├── docker/
│ ├── docker-compose.yml
│ ├── .env.backend
│ └── nginx.conf
└── backups/
└── db/ # Databázové exporty
cd ~/projects/citya-local/repos
# Backend
git clone https://dev.azure.com/citya-team/Citya/_git/Backend backend
# Dispatcher
git clone https://dev.azure.com/citya-team/Citya/_git/Dispatcher dispatcher
git clone git@gitea:citya/backend.git backend
git clone git@gitea:citya/dispatcher.git dispatcher
CITYA vyžaduje PostgreSQL s rozšířením PostGIS pro práci s geografickými daty.
mkdir -p ~/projects/citya-local/docker/postgres
cat > ~/projects/citya-local/docker/postgres/Dockerfile << 'EOF'
FROM postgres:13
# Instalace PostGIS
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
postgresql-13-postgis-3 \
postgresql-13-postgis-3-scripts \
&& rm -rf /var/lib/apt/lists/*
# Inicializační skript pro vytvoření rozšíření
RUN mkdir -p /docker-entrypoint-initdb.d
COPY init-postgis.sh /docker-entrypoint-initdb.d/
EOF
cat > ~/projects/citya-local/docker/postgres/init-postgis.sh << 'EOF'
#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS postgis_topology;
CREATE EXTENSION IF NOT EXISTS fuzzystrmatch;
CREATE EXTENSION IF NOT EXISTS postgis_tiger_geocoder;
EOSQL
EOF
chmod +x ~/projects/citya-local/docker/postgres/init-postgis.sh
cd ~/projects/citya-local/docker/postgres
docker build -t citya-postgis:13 .
Nejprve se připojte k GCP a zjistěte dostupné instance:
# Přihlášení do GCP
gcloud auth login
gcloud config set project citya-development
# Seznam Cloud SQL instancí
gcloud sql instances list
# Vytvořte export bucket (pokud neexistuje)
gsutil mb gs://citya-local-backup
# Export databáze do GCS
gcloud sql export sql INSTANCE_NAME gs://citya-local-backup/citya-export.sql \
--database=citya \
--offload
# Stažení exportu
mkdir -p ~/projects/citya-local/backups/db
gsutil cp gs://citya-local-backup/citya-export.sql ~/projects/citya-local/backups/db/
# Instalace Cloud SQL Proxy
curl -o cloud-sql-proxy https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.8.0/cloud-sql-proxy.linux.amd64
chmod +x cloud-sql-proxy
# Spuštění proxy (v novém terminálu)
./cloud-sql-proxy --port 5432 citya-development:europe-west1:citya-stage &
# Export pomocí pg_dump
PGPASSWORD='your_password' pg_dump -h 127.0.0.1 -U postgres -d citya \
--no-owner --no-acl \
-f ~/projects/citya-local/backups/db/citya-export.sql
Nejprve spusťte PostgreSQL kontejner:
docker run -d \
--name citya-postgres-temp \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=citya \
-p 5433:5432 \
-v citya_db_data:/var/lib/postgresql/data \
citya-postgis:13
# Počkejte na inicializaci
sleep 10
Import dat:
# Import SQL dumpu
PGPASSWORD=postgres psql -h localhost -p 5433 -U postgres -d citya \
-f ~/projects/citya-local/backups/db/citya-export.sql
# Ověření importu
PGPASSWORD=postgres psql -h localhost -p 5433 -U postgres -d citya -c "
SELECT table_name,
(SELECT COUNT(*) FROM information_schema.columns WHERE table_name = t.table_name) as columns
FROM information_schema.tables t
WHERE table_schema = 'public'
ORDER BY table_name;
"
Zastavte dočasný kontejner:
docker stop citya-postgres-temp
docker rm citya-postgres-temp
Redis je použit pro caching a session management. Konfigurace je součástí docker-compose.yml.
Backend vyžaduje curl pro health check. Upravte Dockerfile:
cat > ~/projects/citya-local/repos/backend/src/ApiEntrypoint/Dockerfile << 'EOF'
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 8080
# Instalace curl pro health check a nastavení timezone
RUN apt-get update && \
apt-get install -y --no-install-recommends tzdata curl && \
rm -rf /var/lib/apt/lists/*
ENV TZ=Europe/Prague
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["src/ApiEntrypoint/ApiEntrypoint.csproj", "src/ApiEntrypoint/"]
COPY ["Citya.Backend/Citya.Backend.csproj", "Citya.Backend/"]
COPY ["Citya.Core/Citya.Core.csproj", "Citya.Core/"]
RUN dotnet restore "src/ApiEntrypoint/ApiEntrypoint.csproj"
COPY . .
WORKDIR "/src/src/ApiEntrypoint"
RUN dotnet build "ApiEntrypoint.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "ApiEntrypoint.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
ENTRYPOINT ["dotnet", "ApiEntrypoint.dll"]
EOF
Vytvořte soubor s proměnnými prostředí:
cat > ~/projects/citya-local/docker/.env.backend << 'EOF'
# Database
POSTGRES_CONNECTION_STRING=Host=database;Port=5432;Database=citya;Username=postgres;Password=postgres
POSTGRES_CONNECTION_STRING_READONLY=Host=database;Port=5432;Database=citya;Username=postgres;Password=postgres
# Redis
REDIS_CONNECTION_STRING=redis:6379
# URLs
BACKEND_URL=https://citya-local-api.mberanek.cz
# Google Maps API Keys (získat z GCP Secret Manager)
DISPATCHER_GOOGLE_MAPS_API_KEY=YOUR_DISPATCHER_MAPS_API_KEY
PAS_GOOGLE_MAPS_API_KEY=YOUR_PAS_MAPS_API_KEY
# External Services (placeholders pro lokální vývoj)
CEDA_ACCESS_TOKEN=placeholder-ceda-token
STRIPE_API_KEY=sk_test_placeholder
VONAGE_API_KEY=placeholder-vonage-key
VONAGE_API_SECRET=placeholder-vonage-secret
GO_SMS_CLIENT_ID=placeholder-gosms-id
GO_SMS_CLIENT_SECRET=placeholder-gosms-secret
GO_SMS_CHANNEL_ID=1
# Environment
ASPNETCORE_ENVIRONMENT=Development
TENANT=citya-local
EOF
Dispatcher je Vue.js/Quasar SPA. Upravte Dockerfile pro podporu konfigurovatelných proměnných:
cat > ~/projects/citya-local/repos/dispatcher/Dockerfile << 'EOF'
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Install yarn
RUN corepack enable && corepack prepare yarn@1.22.22 --activate
# Copy all source files first (Quasar needs project context)
COPY . .
# Install dependencies (postinstall runs quasar prepare)
RUN yarn install --frozen-lockfile
# Build arguments for environment configuration
ARG VITE_API_URL=http://localhost:8085
ARG VITE_TENANT=citya-local
ARG VITE_ENV=development
ARG VITE_CDN_BUCKET=citya-local-cdn
ARG VITE_CDN_URL=
# Set environment variables for build
ENV VITE_API_URL=$VITE_API_URL
ENV VITE_TENANT=$VITE_TENANT
ENV VITE_ENV=$VITE_ENV
ENV VITE_CDN_BUCKET=$VITE_CDN_BUCKET
ENV VITE_CDN_URL=$VITE_CDN_URL
# Build the application
RUN yarn build
# Production stage
FROM nginx:alpine AS production
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy built assets from builder stage
COPY --from=builder /app/dist/spa /usr/share/nginx/html
# Add healthcheck
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:80/ || exit 1
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
EOF
cat > ~/projects/citya-local/repos/dispatcher/nginx.conf << 'EOF'
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# SPA routing - all routes go to index.html
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# CDN proxy (optional - for local CDN files)
location /cdn/ {
alias /usr/share/nginx/html/cdn/;
}
}
EOF
Pro podporu lokálního CDN upravte src/stores/cdn.store.ts:
// Změňte řádek s cdnUrl na:
const cdnUrl = import.meta.env.VITE_CDN_URL || `https://storage.googleapis.com/${import.meta.env.VITE_CDN_BUCKET}`
Pro HTTPS přístup s automatickými certifikáty použijte Traefik:
cat > ~/projects/citya-local/docker/traefik.yml << 'EOF'
api:
dashboard: true
insecure: true
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
certificatesResolvers:
letsencrypt:
acme:
email: your-email@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
providers:
docker:
exposedByDefault: false
network: citya-network
log:
level: INFO
EOF
cat > ~/projects/citya-local/docker/docker-compose.yml << 'EOF'
services:
traefik:
image: traefik:v3.0
container_name: citya-traefik
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- traefik_letsencrypt:/letsencrypt
networks:
- citya-network
restart: unless-stopped
database:
container_name: citya-postgres
image: citya-postgis:13
ports:
- "5433:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: citya
volumes:
- citya_db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d citya"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- citya-network
redis:
container_name: citya-redis
image: redis:7-alpine
ports:
- "6380:6379"
volumes:
- citya_redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- citya-network
backend:
container_name: citya-backend
build:
context: ../repos/backend
dockerfile: src/ApiEntrypoint/Dockerfile
image: citya-backend:latest
ports:
- "8085:8080"
env_file:
- .env.backend
environment:
ASPNETCORE_ENVIRONMENT: Development
ASPNETCORE_URLS: http://+:8080
depends_on:
database:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
networks:
- citya-network
labels:
- "traefik.enable=true"
# HTTPS Router
- "traefik.http.routers.citya-local-api.rule=Host(`citya-local-api.mberanek.cz`)"
- "traefik.http.routers.citya-local-api.entrypoints=websecure"
- "traefik.http.routers.citya-local-api.tls=true"
- "traefik.http.routers.citya-local-api.tls.certresolver=letsencrypt"
- "traefik.http.routers.citya-local-api.middlewares=citya-local-api-cors"
- "traefik.http.services.citya-local-api.loadbalancer.server.port=8080"
# CORS Middleware
- "traefik.http.middlewares.citya-local-api-cors.headers.accesscontrolallowmethods=GET,POST,PUT,DELETE,PATCH,OPTIONS"
- "traefik.http.middlewares.citya-local-api-cors.headers.accesscontrolallowheaders=Content-Type,Authorization,X-Requested-With,Area,area,App,app"
- "traefik.http.middlewares.citya-local-api-cors.headers.accesscontrolalloworiginlist=https://citya-local.mberanek.cz"
- "traefik.http.middlewares.citya-local-api-cors.headers.accesscontrolallowcredentials=true"
- "traefik.http.middlewares.citya-local-api-cors.headers.accesscontrolmaxage=100"
dispatcher:
container_name: citya-dispatcher
build:
context: ../repos/dispatcher
dockerfile: Dockerfile
args:
VITE_API_URL: https://citya-local-api.mberanek.cz
VITE_TENANT: citya-local
VITE_ENV: development
VITE_CDN_URL: https://citya-local.mberanek.cz/cdn
image: citya-dispatcher:latest
ports:
- "8086:80"
depends_on:
backend:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:80/"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
networks:
- citya-network
labels:
- "traefik.enable=true"
- "traefik.http.routers.citya-local-dispatcher.rule=Host(`citya-local.mberanek.cz`)"
- "traefik.http.routers.citya-local-dispatcher.entrypoints=websecure"
- "traefik.http.routers.citya-local-dispatcher.tls=true"
- "traefik.http.routers.citya-local-dispatcher.tls.certresolver=letsencrypt"
- "traefik.http.services.citya-local-dispatcher.loadbalancer.server.port=80"
networks:
citya-network:
name: citya-network
driver: bridge
volumes:
citya_db_data:
citya_redis_data:
traefik_letsencrypt:
EOF
cd ~/projects/citya-local/docker
# Build a spuštění
docker compose up -d --build
# Sledování logů
docker compose logs -f
# Kontrola stavu
docker compose ps
Po spuštění dispatcheru vytvořte potřebné CDN soubory:
docker exec citya-dispatcher sh -c "mkdir -p /usr/share/nginx/html/cdn && echo '{\"release\":{\"from\":null,\"to\":null},\"versions\":{\"minSupported\":{\"passApp\":\"1.0.0\"}}}' > /usr/share/nginx/html/cdn/remote-config.json"
Pro přihlášení do dispatcheru potřebujete vytvořit uživatele v databázi:
# Generování BCrypt hashe pro heslo (např. "admin123")
# Můžete použít online nástroj nebo tento příkaz v Node.js:
# node -e "console.log(require('bcryptjs').hashSync('admin123', 10))"
# Hash pro heslo "admin123": $2a$10$N9qo8uLOickgx2ZMRZoMy.MqrqVqzBqVj1v6F9qKjqKjqKjqKjqKj
PGPASSWORD=postgres psql -h localhost -p 5433 -U postgres -d citya << 'EOF'
INSERT INTO "User" (
"Id",
"Email",
"NormalizedEmail",
"PasswordHash",
"FirstName",
"LastName",
"PhoneNumber",
"Role",
"Status",
"CreatedAt",
"UpdatedAt"
) VALUES (
gen_random_uuid(),
'localadmin@citya.local',
'LOCALADMIN@CITYA.LOCAL',
'$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
'Local',
'Admin',
'+420000000000',
'Admin',
0,
NOW(),
NOW()
);
EOF
Poznámka: Hash
$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igiodpovídá heslupassword. Pro produkci použijte silnější heslo.
Uživatel musí být přiřazen k oblastem (areas) pro zobrazení dat:
PGPASSWORD=postgres psql -h localhost -p 5433 -U postgres -d citya << 'EOF'
-- Najít ID uživatele
DO $$
DECLARE
user_id UUID;
BEGIN
SELECT "Id" INTO user_id FROM "User" WHERE "Email" = 'localadmin@citya.local';
-- Přiřadit ke všem oblastem
INSERT INTO "GeoAreaUser" ("GeoAreasId", "UsersId")
SELECT "Id", user_id FROM "GeoArea"
ON CONFLICT DO NOTHING;
END $$;
EOF
PGPASSWORD=postgres psql -h localhost -p 5433 -U postgres -d citya -c "
SELECT u.\"Email\", u.\"Role\", u.\"Status\", COUNT(gau.\"GeoAreasId\") as areas
FROM \"User\" u
LEFT JOIN \"GeoAreaUser\" gau ON u.\"Id\" = gau.\"UsersId\"
WHERE u.\"Email\" = 'localadmin@citya.local'
GROUP BY u.\"Id\";
"
# Přihlášení do GCP
gcloud auth login
gcloud config set project citya-development
# Výpis dostupných secrets
gcloud secrets list
# Získání Maps API klíče
gcloud secrets versions access latest --secret=citya-development-main-secrets | grep DISPATCHER_GOOGLE_MAPS_API_KEY
Aktualizujte .env.backend s reálným API klíčem:
sed -i 's/DISPATCHER_GOOGLE_MAPS_API_KEY=.*/DISPATCHER_GOOGLE_MAPS_API_KEY=YOUR_ACTUAL_API_KEY/' ~/projects/citya-local/docker/.env.backend
cd ~/projects/citya-local/docker
docker compose up -d backend
Dispatcher přistupuje k cachovaným geocoding datům v GCS bucket citya-revgeo-storage. Pro přístup z vaší domény je potřeba nastavit CORS:
# Aktuální CORS nastavení
gsutil cors get gs://citya-revgeo-storage
# Vytvoření CORS konfigurace
cat > /tmp/cors.json << 'EOF'
[
{
"origin": [
"https://citya-local.mberanek.cz",
"http://localhost:9000",
"http://localhost:9200"
],
"method": ["GET"],
"maxAgeSeconds": 3600
}
]
EOF
# Aplikace CORS
gsutil cors set /tmp/cors.json gs://citya-revgeo-storage
Poznámka: Pokud nemáte přístup k GCS bucket, revgeo cache nebude fungovat. Aplikace bude zobrazovat souřadnice místo adres, což je akceptovatelné pro lokální vývoj.
docker compose ps
Očekávaný výstup:
NAME STATUS PORTS
citya-backend Up X minutes (healthy) 0.0.0.0:8085->8080/tcp
citya-dispatcher Up X minutes (healthy) 0.0.0.0:8086->80/tcp
citya-postgres Up X minutes (healthy) 0.0.0.0:5433->5432/tcp
citya-redis Up X minutes (healthy) 0.0.0.0:6380->6379/tcp
citya-traefik Up X minutes 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
# Health check
curl -s http://localhost:8085/health
# CORS test
curl -s -I -X OPTIONS \
-H "Origin: https://citya-local.mberanek.cz" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: Authorization,area" \
https://citya-local-api.mberanek.cz/api/areas
Očekávané CORS hlavičky:
access-control-allow-credentials: true
access-control-allow-headers: Content-Type,Authorization,X-Requested-With,Area,area,App,app
access-control-allow-methods: GET,POST,PUT,DELETE,PATCH,OPTIONS
access-control-allow-origin: https://citya-local.mberanek.cz
localadmin@citya.local / passwordSymptom: Backend kontejner se restartuje s chybou o chybějících proměnných.
Řešení:
# Zkontrolujte logy
docker logs citya-backend
# Ověřte, že .env.backend existuje a je správně načten
docker compose config | grep -A20 backend
Symptom: Access-Control-Allow-Origin chyby v browser konzoli.
Řešení:
docker compose up -d backendSymptom: Dispatcher health check selhává s connection refused.
Řešení: Použijte 127.0.0.1 místo localhost v health check:
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:80/"]
Symptom: Login vrací chybu i se správnými údaji.
Řešení:
$2a$, ne $2b$)0 = Active):UPDATE "User" SET "Status" = 0 WHERE "Email" = 'localadmin@citya.local';
Symptom: Po přihlášení nejsou vidět žádné rides, areas, companies.
Řešení: Uživatel musí být přiřazen k oblastem:
-- Zkontrolujte přiřazení
SELECT * FROM "GeoAreaUser" WHERE "UsersId" = (
SELECT "Id" FROM "User" WHERE "Email" = 'localadmin@citya.local'
);
-- Přiřaďte ke všem oblastem
INSERT INTO "GeoAreaUser" ("GeoAreasId", "UsersId")
SELECT "Id", (SELECT "Id" FROM "User" WHERE "Email" = 'localadmin@citya.local')
FROM "GeoArea"
ON CONFLICT DO NOTHING;
Symptom: Chyby typu Failed to fetch dynamically imported module nebo 404 na JS soubory.
Řešení: Vyčistěte cache prohlížeče:
Cmd+Shift+R (Mac) nebo Ctrl+Shift+R (Win/Linux)Cmd+Option+E pak Cmd+RSymptom: Mapy se nezobrazují, v konzoli InvalidKeyMapError.
Řešení:
.env.backendSymptom: Požadavky na citya-revgeo-storage vracejí 404.
Vysvětlení: Toto je očekávané chování - revgeo cache obsahuje pouze adresy z produkce/stage. Pro lokální vývoj není kritické.
| Služba | Interní port | Externí port | URL |
|---|---|---|---|
| PostgreSQL | 5432 | 5433 | localhost:5433 |
| Redis | 6379 | 6380 | localhost:6380 |
| Backend API | 8080 | 8085 | http://localhost:8085 |
| Dispatcher | 80 | 8086 | http://localhost:8086 |
| Traefik Dashboard | 8080 | 8080 | http://localhost:8080 |
| HTTPS (Traefik) | 443 | 443 | https://your-domain.com |
# Restart všech služeb
docker compose restart
# Rebuild a restart konkrétní služby
docker compose up -d --build dispatcher
# Zobrazení logů
docker compose logs -f backend
# Připojení do databáze
docker exec -it citya-postgres psql -U postgres -d citya
# Připojení do Redis
docker exec -it citya-redis redis-cli
# Vyčištění všech dat (POZOR - smaže vše!)
docker compose down -v
Poslední aktualizace: Leden 2026