Ebook Syafi
Koleksi
Admin
← DevOps Dari Homelab ke Production
Edit Bab
💾 Simpan
Batal
Syarat
Mukadimah
Bab
Penutup
B
I
H2
H3
List
1.
Quote
Code
Link
Img
Table
Edit
Split
Preview
0 perkataan
# Bab 4: Docker untuk Production Anda sudah tahu Docker. Anda boleh tulis Dockerfile, run `docker compose up`, dan deploy aplikasi di homelab anda. Itu bagus. Tapi production adalah binatang yang berbeza sama sekali. Di homelab, kalau container crash tengah malam, anda bangun esok pagi dan restart. Di production, kalau container crash tengah malam, customer call support, support escalate ke manager, manager call anda. Tidak fun. Bab ini bukan tentang belajar Docker dari awal. Anda sudah lepas tahap itu. Bab ini tentang bagaimana run Docker dengan yakin di production. Bagaimana buat image yang kecil dan selamat. Bagaimana monitor containers. Bagaimana pastikan semuanya berjalan walaupun anda sedang tidur. ## Apa yang anda akan belajar: - Multi-stage builds untuk image yang optimized - Teknik optimization Docker image (saiz, layers, security) - Docker registry: Harbor dan GitHub Container Registry - Docker Compose untuk production vs development - Docker networking dalam konteks production - Container health checks yang berkesan - Strategi logging untuk containers - Docker security best practices - Resource limits dan kenapa ia penting ## Multi-Stage Builds Ini adalah teknik pertama yang anda perlu kuasai untuk production. Multi-stage builds membolehkan anda gunakan satu Dockerfile untuk build dan hasilkan image yang kecil. Masalah dengan Dockerfile biasa: anda install build tools (compiler, dev dependencies) untuk build aplikasi, tetapi tools ini tak diperlukan untuk run aplikasi. Hasilnya? Image yang besar tanpa sebab. ### Contoh: Node.js ```dockerfile # Stage 1: Build FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build RUN npm prune --production # Stage 2: Production FROM node:20-alpine AS production RUN addgroup -g 1001 -S appgroup && \ adduser -S appuser -u 1001 -G appgroup WORKDIR /app COPY --from=builder --chown=appuser:appgroup /app/dist ./dist COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules COPY --from=builder --chown=appuser:appgroup /app/package.json ./ USER appuser EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 CMD ["node", "dist/index.js"] ``` Perhatikan apa yang berlaku di sini. Stage pertama install semua dependencies (termasuk devDependencies), build aplikasi, kemudian prune devDependencies. Stage kedua hanya copy files yang diperlukan. Hasilnya? Image yang jauh lebih kecil. ### Contoh: Go Go adalah contoh terbaik untuk multi-stage builds kerana Go binary adalah standalone. Anda boleh compile di satu stage dan copy binary ke image yang sangat minimal. ```dockerfile # Stage 1: Build FROM golang:1.22-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server # Stage 2: Production FROM scratch COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /app/server /server EXPOSE 8080 ENTRYPOINT ["/server"] ``` Image dari `scratch` hanya mengandungi binary anda dan SSL certificates. Saiznya boleh jadi serendah 10 hingga 15 MB berbanding ratusan MB kalau guna image biasa. > **Nota Beginner:** `scratch` adalah "empty" image. Ia betul-betul kosong, tiada shell, tiada tools, tiada apa-apa. Sesuai untuk Go, Rust, atau mana-mana compiled language yang menghasilkan static binary. Tapi kalau anda perlu debug, gunakan `alpine` sebagai base image. ### Contoh: Python ```dockerfile # Stage 1: Build FROM python:3.12-slim AS builder WORKDIR /app RUN python -m venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Stage 2: Production FROM python:3.12-slim AS production RUN groupadd -r appgroup && useradd -r -g appgroup appuser COPY --from=builder /opt/venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" WORKDIR /app COPY --chown=appuser:appgroup . . USER appuser EXPOSE 8000 CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "app:create_app()"] ``` ## Docker Image Optimization Image size bukan sekadar soal disk space. Image yang kecil bermaksud pull lebih cepat, deploy lebih cepat, dan attack surface yang lebih kecil. ### Pilih Base Image yang Sesuai ``` node:20 ~ 1.1 GB node:20-slim ~ 200 MB node:20-alpine ~ 130 MB ``` Perbezaan ini significant. Kalau anda deploy 10 kali sehari, bayangkan berapa bandwidth yang anda jimat dengan `alpine`. Namun, `alpine` menggunakan `musl` dan bukannya `glibc`. Sesetengah native dependencies mungkin tidak serasi. Kalau anda menghadapi masalah compatibility, `slim` adalah pilihan yang baik sebagai jalan tengah. ### Optimumkan Layer Caching Docker build setiap instruction dalam Dockerfile sebagai satu layer. Kalau satu layer berubah, semua layer selepasnya perlu rebuild. Jadi, susun instructions anda supaya yang jarang berubah berada di atas. ```dockerfile # BAIK: Dependencies dulu, source code kemudian COPY package*.json ./ RUN npm ci COPY . . # KURANG BAIK: Semua sekali COPY . . RUN npm ci ``` Dalam contoh pertama, kalau anda hanya ubah source code tanpa ubah dependencies, Docker boleh guna cached layer untuk `npm ci`. Ini boleh jimat beberapa minit dalam setiap build. ### Gunakan .dockerignore Sama macam `.gitignore`, tapi untuk Docker. Pastikan anda tak copy file yang tak perlu ke dalam image. ``` # .dockerignore node_modules .git .gitignore *.md docker-compose*.yml .env* tests/ coverage/ .github/ ``` ## Docker Registry Di homelab, mungkin anda cuma buat `docker build` dan terus run. Di production, anda perlukan registry untuk simpan dan distribute images. ### GitHub Container Registry (GHCR) Kalau code anda di GitHub, GHCR adalah pilihan paling mudah. Ia integrate terus dengan GitHub Actions dan menggunakan `GITHUB_TOKEN` yang sedia ada. ```bash # Login echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin # Tag dan push docker tag my-app:latest ghcr.io/username/my-app:v1.0.0 docker push ghcr.io/username/my-app:v1.0.0 ``` ### Harbor: Self-Hosted Registry Untuk organisasi yang memerlukan lebih kawalan, Harbor adalah pilihan yang excellent. Ia open source, mempunyai vulnerability scanning built-in, dan boleh di-host sendiri. ```yaml # docker-compose untuk Harbor (simplified) # Untuk production, gunakan installer rasmi dari Harbor version: '3' services: registry: image: goharbor/harbor-core:v2.10.0 ports: - "443:8443" volumes: - harbor-data:/data ``` > **Nota Beginner:** Untuk permulaan, GHCR sudah lebih dari cukup. Anda cuma perlukan self-hosted registry bila ada keperluan specific seperti compliance, air-gapped environment, atau anda perlu vulnerability scanning yang lebih mendalam. ### Image Tagging Strategy Bagaimana anda tag images sangat penting untuk production. Ini beberapa pendekatan yang biasa digunakan. **Semantic Versioning** menggunakan format `v1.2.3`. Major version untuk breaking changes, minor untuk features baru, patch untuk bug fixes. Ini paling mudah difahami oleh manusia. **Git SHA** menggunakan commit hash sebagai tag, contohnya `abc123f`. Setiap image boleh di-trace balik ke exact commit. Sangat berguna untuk debugging. **Gabungan kedua-duanya** adalah pendekatan terbaik. Tag image anda dengan kedua-dua semantic version dan Git SHA. ```bash # Tag dengan multiple tags docker tag my-app:latest ghcr.io/username/my-app:v1.2.3 docker tag my-app:latest ghcr.io/username/my-app:abc123f docker tag my-app:latest ghcr.io/username/my-app:latest # Push semua tags docker push ghcr.io/username/my-app:v1.2.3 docker push ghcr.io/username/my-app:abc123f docker push ghcr.io/username/my-app:latest ``` Untuk production deployment, selalu guna specific version tag. Gunakan `latest` hanya untuk development atau testing. ### Cleanup Old Images Registry boleh membesar dengan cepat kalau anda tidak cleanup. Set up retention policy untuk automatically delete old images. ```bash # Untuk GHCR, boleh guna GitHub Actions # Delete images yang lebih dari 30 hari dan bukan tagged version - uses: snok/container-retention-policy@v2 with: image-names: my-app cut-off: 30 days ago UTC keep-at-least: 5 account-type: personal token: ${{ secrets.GITHUB_TOKEN }} ``` ## Docker Compose: Production vs Development Anda mungkin sudah biasa dengan satu `docker-compose.yml`. Untuk production, anda patut ada setup yang berbeza. ### Development ```yaml # docker-compose.yml (development) services: app: build: . ports: - "3000:3000" volumes: - .:/app - /app/node_modules environment: - NODE_ENV=development - DEBUG=app:* command: npm run dev db: image: postgres:16 ports: - "5432:5432" environment: - POSTGRES_PASSWORD=devpassword volumes: - pgdata:/var/lib/postgresql/data volumes: pgdata: ``` ### Production ```yaml # docker-compose.prod.yml services: app: image: ghcr.io/username/my-app:${IMAGE_TAG:-latest} restart: always ports: - "3000:3000" environment: - NODE_ENV=production env_file: - .env.production deploy: resources: limits: cpus: '1.0' memory: 512M reservations: cpus: '0.25' memory: 128M healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"] interval: 30s timeout: 5s retries: 3 start_period: 15s logging: driver: "json-file" options: max-size: "10m" max-file: "5" db: image: postgres:16-alpine restart: always volumes: - pgdata:/var/lib/postgresql/data environment: POSTGRES_PASSWORD_FILE: /run/secrets/db_password secrets: - db_password deploy: resources: limits: cpus: '2.0' memory: 1G secrets: db_password: file: ./secrets/db_password.txt volumes: pgdata: driver: local ``` Nampak perbezaan? Production version tidak mount source code (guna pre-built image), tidak expose database port ke host, mempunyai resource limits, health checks, proper logging configuration, dan menggunakan secrets untuk sensitive data. Ini semua perkara kecil yang membezakan homelab setup dengan production setup. ## Docker Networking dalam Production Di homelab, semua container biasanya dalam satu network yang sama. Di production, anda perlu lebih berhati-hati. ```yaml services: nginx: image: nginx:alpine networks: - frontend ports: - "80:80" - "443:443" app: image: my-app:latest networks: - frontend - backend db: image: postgres:16 networks: - backend redis: image: redis:7-alpine networks: - backend networks: frontend: driver: bridge backend: driver: bridge internal: true ``` Perhatikan `internal: true` pada network `backend`. Ini bermaksud containers dalam network ini tidak boleh access internet secara langsung. Database dan Redis hanya boleh diakses oleh app container melalui internal network. Nginx boleh access app melalui frontend network, tapi tidak boleh access database secara langsung. Ini adalah prinsip **least privilege** yang penting untuk security. ## Container Health Checks Health checks adalah cara Docker (atau orchestrator) tahu sama ada container anda sihat. Tanpa health check, Docker hanya tahu container running atau tidak. Ia tak tahu kalau aplikasi anda stuck dalam infinite loop atau database connection putus. ```dockerfile # Dalam Dockerfile HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 ``` Dan endpoint health check anda perlu bermakna: ```javascript // Node.js health check endpoint app.get('/health', async (req, res) => { try { // Check database connection await db.query('SELECT 1'); // Check Redis connection await redis.ping(); res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), checks: { database: 'connected', redis: 'connected' } }); } catch (error) { res.status(503).json({ status: 'unhealthy', error: error.message }); } }); ``` > **Nota Beginner:** Parameter `--start-period` penting untuk aplikasi yang mengambil masa untuk start up. Selama tempoh ini, health check failures tidak dikira. Tanpa ini, Docker mungkin restart container sebelum ia sempat start. ## Strategi Logging Di homelab, anda mungkin cuma `docker logs` dan scroll. Di production, anda perlukan logging yang lebih terstruktur. ### Structured Logging Tulis log dalam format JSON supaya mudah di-parse oleh tools seperti ELK Stack atau Loki. ```javascript // Daripada ini: console.log('User logged in: john@example.com'); // Gunakan ini: logger.info({ event: 'user_login', email: 'john@example.com', ip: req.ip, timestamp: new Date().toISOString() }); ``` ### Docker Logging Drivers ```yaml # docker-compose.prod.yml services: app: logging: driver: "json-file" options: max-size: "10m" max-file: "5" labels: "app" tag: "{{.Name}}" ``` Untuk setup yang lebih advanced, anda boleh forward logs ke centralized logging system: ```yaml services: app: logging: driver: "fluentd" options: fluentd-address: "localhost:24224" tag: "app.{{.Name}}" ``` Penting untuk set `max-size` dan `max-file`. Tanpa limit ini, log files boleh memenuhi disk anda. Saya pernah lihat server yang disk penuh kerana log files. Semua services down hanya kerana tak ada disk space. Jangan jadi macam tu. ### Log Levels Pastikan anda gunakan log levels yang betul. Ini memudahkan filtering di production. ```javascript // Gunakan library seperti winston atau pino const logger = require('pino')({ level: process.env.LOG_LEVEL || 'info' }); logger.debug('Detailed debugging info'); // Hanya untuk development logger.info('User created successfully'); // Normal operations logger.warn('Disk usage above 80%'); // Perlu perhatian logger.error('Database connection failed'); // Ada masalah logger.fatal('Application crashed'); // Kritikal ``` Di production, set log level ke `info` atau `warn`. Jangan gunakan `debug` di production kerana ia akan menghasilkan terlalu banyak log dan boleh menjejaskan performance. ### Centralized Logging dengan Loki Untuk setup yang lebih lengkap, anda boleh gunakan Grafana Loki untuk centralized logging. Ia lebih lightweight berbanding ELK Stack dan integrate dengan baik bersama Grafana untuk visualization. ```yaml services: loki: image: grafana/loki:2.9.0 ports: - "3100:3100" volumes: - loki-data:/loki promtail: image: grafana/promtail:2.9.0 volumes: - /var/log:/var/log - /var/lib/docker/containers:/var/lib/docker/containers:ro command: -config.file=/etc/promtail/config.yml grafana: image: grafana/grafana:latest ports: - "3000:3000" environment: - GF_SECURITY_ADMIN_PASSWORD=admin ``` > **Nota Beginner:** Kalau anda baru bermula, `json-file` logging driver dengan `max-size` limit sudah memadai. Anda boleh upgrade ke centralized logging bila anda ada multiple servers atau bila anda rasa sukar untuk troubleshoot issues dengan `docker logs` sahaja. ## Docker Security Best Practices Security di production bukan optional. Ini checklist yang anda perlu ikut. ### 1. Jangan Run sebagai Root ```dockerfile # Buat user baru RUN addgroup -g 1001 -S appgroup && \ adduser -S appuser -u 1001 -G appgroup # Tukar kepada user tersebut USER appuser ``` ### 2. Gunakan Read-Only Filesystem ```yaml services: app: read_only: true tmpfs: - /tmp - /app/temp ``` Ini memastikan tiada siapa (termasuk attacker) boleh write ke filesystem container. Kalau aplikasi anda perlu write ke temporary files, gunakan `tmpfs`. ### 3. Drop Unnecessary Capabilities ```yaml services: app: cap_drop: - ALL cap_add: - NET_BIND_SERVICE security_opt: - no-new-privileges:true ``` ### 4. Scan Images untuk Vulnerabilities ```bash # Gunakan Docker Scout docker scout cves my-app:latest # Atau Trivy (open source) trivy image my-app:latest ``` Integrate scanning ini dalam CI/CD pipeline supaya setiap image di-scan sebelum deploy: ```yaml # Dalam GitHub Actions - name: Scan image uses: aquasecurity/trivy-action@master with: image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }} exit-code: '1' severity: 'CRITICAL,HIGH' ``` ### 5. Gunakan Specific Tags, Bukan Latest ```dockerfile # KURANG BAIK FROM node:latest # BAIK FROM node:20.11-alpine ``` `latest` boleh berubah pada bila-bila masa. Satu hari semuanya berjalan lancar, hari berikutnya image berubah dan aplikasi anda rosak. Pin your versions. ### 6. Jangan Simpan Secrets dalam Image Ini kesilapan yang kerap berlaku. Jangan copy `.env` file atau secrets ke dalam Docker image. Walaupun anda delete file tersebut dalam layer seterusnya, ia masih boleh diakses melalui layer sebelumnya. ```dockerfile # SALAH - secret masih ada dalam image layers COPY .env . RUN source .env && npm run build RUN rm .env # BETUL - guna build arguments untuk build-time secrets ARG API_ENDPOINT ENV API_ENDPOINT=$API_ENDPOINT RUN npm run build # LEBIH BETUL - guna Docker BuildKit secrets RUN --mount=type=secret,id=env,target=/app/.env npm run build ``` Untuk runtime secrets, gunakan environment variables atau Docker secrets, bukan files yang di-copy ke dalam image. ### 7. Keep Images Updated Set up automated process untuk rebuild images bila base image ada security update. Tools seperti Dependabot atau Renovate boleh bantu automate ini. Anda juga boleh schedule weekly rebuilds dalam CI/CD pipeline untuk pastikan base images sentiasa up to date. ## Resource Limits Ini adalah satu perkara yang sering diabaikan tetapi sangat kritikal. Tanpa resource limits, satu container yang leak memory boleh menjatuhkan keseluruhan server. ```yaml services: app: deploy: resources: limits: cpus: '1.0' memory: 512M reservations: cpus: '0.25' memory: 128M ``` **limits** bermaksud maximum resources yang container boleh guna. Kalau container cuba guna lebih dari 512MB memory, Docker akan kill container tersebut (OOM kill). **reservations** bermaksud minimum resources yang dijamin untuk container. Docker akan pastikan sekurang-kurangnya resources ini tersedia. Untuk menentukan resource limits yang sesuai, monitor aplikasi anda terlebih dahulu. Jalankan ia tanpa limits di staging environment, perhatikan berapa banyak CPU dan memory ia guna dalam keadaan normal dan di bawah load. Kemudian set limits dengan buffer yang munasabah, mungkin 1.5 hingga 2 kali ganda penggunaan normal. > **Nota Beginner:** Kalau anda nampak container anda kerap di-OOM kill, jangan terus naikkan memory limit. Mungkin ada memory leak dalam aplikasi anda. Check dan fix root cause dulu. ## Container Restart Policies Di homelab, kalau container crash, anda mungkin manually restart. Di production, anda perlukan restart policy yang automatik. ```yaml services: app: restart: always # Sentiasa restart, kecuali manually stopped worker: restart: on-failure # Restart hanya kalau exit code bukan 0 migration: restart: "no" # Jangan restart (untuk one-off tasks) ``` Bila guna yang mana? - **always** sesuai untuk long-running services seperti web server dan API. Ia akan restart walaupun selepas server reboot. - **on-failure** sesuai untuk background workers yang mungkin crash sesekali. Ia akan restart kalau process exit dengan error, tapi tidak kalau ia exit secara normal. - **no** sesuai untuk tasks yang hanya perlu run sekali, seperti database migration atau data seeding. Ada juga `unless-stopped` yang sama macam `always`, tapi tidak restart selepas anda manually stop container. Ini berguna kalau anda perlu stop container untuk maintenance tanpa risau ia restart sendiri. ## Docker Image Layers dan Build Cache Memahami bagaimana Docker layers berfungsi boleh menjimatkan banyak masa build. Setiap instruction dalam Dockerfile menghasilkan satu layer. Docker cache setiap layer. Kalau instruction dan semua layers sebelumnya tidak berubah, Docker guna cached version. Ini bermaksud susun atur instructions anda sangat penting: ```dockerfile # Optimized layer ordering FROM node:20-alpine WORKDIR /app # Layer 1: Jarang berubah COPY package.json package-lock.json ./ # Layer 2: Hanya rebuild bila dependencies berubah RUN npm ci --production # Layer 3: Berubah setiap kali ada code change COPY . . # Layer 4: Build step RUN npm run build ``` Kalau anda cuma ubah source code tanpa ubah dependencies, Docker hanya perlu rebuild Layer 3 dan 4. Layer 1 dan 2 guna cache. Ini boleh jimat beberapa minit dalam setiap build, terutama kalau projek anda ada banyak dependencies. Satu lagi tip: gunakan `.dockerignore` yang comprehensive. Setiap file yang di-COPY ke build context boleh invalidate cache. Kalau anda accidentally copy `node_modules` atau `.git` folder, cache akan invalidate setiap kali walaupun dependencies tidak berubah. ## Backup dan Disaster Recovery Ini satu topik yang ramai orang abaikan sehingga terlambat. Di production, anda perlu plan untuk worst case scenario. ### Backup Database Volumes ```bash # Backup PostgreSQL data docker compose exec db pg_dump -U postgres mydb > backup_$(date +%Y%m%d).sql # Atau backup volume secara langsung docker run --rm \ -v pgdata:/data \ -v $(pwd)/backups:/backup \ alpine tar czf /backup/pgdata_$(date +%Y%m%d).tar.gz -C /data . ``` ### Automated Backup dengan Cron ```yaml services: backup: image: postgres:16-alpine entrypoint: /bin/sh command: > -c "while true; do PGPASSWORD=$$POSTGRES_PASSWORD pg_dump -h db -U postgres mydb > /backups/backup_$$(date +%Y%m%d_%H%M%S).sql; find /backups -name '*.sql' -mtime +7 -delete; sleep 86400; done" environment: POSTGRES_PASSWORD_FILE: /run/secrets/db_password volumes: - ./backups:/backups secrets: - db_password depends_on: - db ``` Script ini akan backup database setiap hari dan delete backups yang lebih dari 7 hari. Simple tapi berkesan. Untuk production yang lebih serius, pertimbangkan untuk upload backups ke cloud storage seperti S3 atau MinIO supaya backups anda tidak hilang kalau server mati. > **Nota Beginner:** Test restore process anda secara berkala. Backup yang tidak boleh di-restore bukan backup. Set up schedule untuk test restore sekurang-kurangnya sebulan sekali. ## Production Checklist Sebelum deploy sebarang container ke production, pastikan anda sudah check perkara berikut: ``` [ ] Multi-stage build digunakan [ ] Base image menggunakan specific version tag [ ] Container tidak run sebagai root [ ] Health check dikonfigurasi [ ] Resource limits ditetapkan [ ] Logging dikonfigurasi dengan size limits [ ] .dockerignore wujud dan lengkap [ ] Secrets tidak di-hardcode dalam image [ ] Image sudah di-scan untuk vulnerabilities [ ] Network segmentation ditetapkan [ ] Restart policy dikonfigurasi [ ] Volumes untuk persistent data dikonfigurasi ``` ## Ringkasan Dalam bab ini, kita telah melangkah jauh dari Docker basics ke Docker production. Multi-stage builds membantu kita buat image yang kecil dan efficient. Proper networking dan security practices memastikan containers kita selamat. Health checks dan resource limits memastikan containers kita reliable. Perkara yang paling penting untuk diingat adalah ini: production Docker bukan sekadar tentang membuat sesuatu berfungsi. Ia tentang membuat sesuatu berfungsi dengan selamat, efficient, dan boleh dipercayai walaupun pada jam 3 pagi. Kita telah melihat perbezaan antara Docker Compose untuk development dan production. Kita telah belajar tentang container registries, logging strategies, dan security hardening. Semua ini adalah building blocks yang anda perlukan sebelum kita masuk ke topik seterusnya: Kubernetes. \newpage