Article · 2022-03-10

From Offline Deployment to Kubernetes Pipeline: A Field Engineer's Practical Summary

Deploying applications in air-gapped environments required a methodical offline deployment approach. Our process centered on these steps:

scp app-1.0.jar [email protected]:/opt/deploy/app-1.0.jar
nohup java -jar /opt/deploy/app-1.0.jar --spring.config.location=/opt/deploy/config/ &

Even if the terminal closes, the application continues running. Log output is redirected to nohup.out or a designated log file.

This process ensured smooth deployment in disconnected networks. However, the manual approach had clear drawbacks: each update required human intervention, multi-server deployments were prone to version inconsistencies and missed steps, and as release frequency increased, the need for a more automated and standardized approach became obvious.

Cassandra Large-Table Export

During operations, we needed to export a high-volume Cassandra table containing hundreds of millions of records as a backup. Without the right tools, this task was genuinely difficult:

COPY keyspace_name.table_name TO 'export.csv';

This command exports query results directly to CSV. However, with hundreds of millions of rows, COPY TO was prohibitively slow and frequently failed midway due to network latency or timeouts, making recovery cumbersome.

dsbulk unload -k keyspace_name -t table_name -url export_data/ -maxRetries 5

DSBulk optimizes reading internally with parallel processing and provides features like resumable exports. In a test, exporting a table of approximately 50 million rows to CSV was reduced from several hours to under one hour—a several-fold improvement in efficiency.

This approach resolved the Cassandra export challenge. Where specialized tools were unavailable, partitioned export was a workable compromise; once professional tools became available, large-scale data migration became both more reliable and faster.

Common Startup Failure Cases

During application deployment and operation, we encountered Java startup failures traceable to third-party components: Atomikos for distributed transaction management and Curator for ZooKeeper client operations. Both cases provide instructive examples of diagnosis and resolution.

Atomikos-Induced Startup Exception

One service used Atomikos as its distributed transaction manager (for multi-datasource transactions). When starting two service instances on the same server, Atomikos initialization threw an exception and blocked startup. The log excerpt:

com.atomikos.icatch.SysException: Error in init: Log already in use? tmlog in ./
    at com.atomikos.icatch.impux.TransactionServiceImp <...> 
Caused by: com.atomikos.recovery.LogException: Log already in use by another process.

The error showed Atomikos unable to create its transaction log file due to a lock conflict (Log already in use). The root cause: multiple applications using Atomikos on the same machine were all trying to use the same default transaction log path, preventing the later-launched process from acquiring the file lock.

Our resolution: We confirmed that the first service was using Atomikos' default transaction log (typically in a transaction-logs folder under the application's working directory). To resolve the conflict, we applied one of two approaches:

spring.jta.atomikos.log-dir=./transaction-logs-app2

The second application's transaction log now writes to a separate directory, eliminating contention with the first.

After applying the log path modification, we restarted the application. Atomikos initialized successfully without further conflicts. This case reminded us that middleware defaults often assume single-instance deployments. When hosting multiple instances on one host, check for shared resource conflicts—files, ports, and similar—and use configuration to isolate them.

Curator-Induced Startup Hang

Another issue involved Apache Curator, a popular ZooKeeper client framework. A microservice used Curator during startup to register with ZooKeeper, but we found the process hung for extended periods in one environment, with logs repeatedly showing:

org.apache.curator.CuratorConnectionLossException: KeeperErrorCode = ConnectionLoss
	at org.apache.curator.ConnectionState.getZooKeeper(ConnectionState.java:123)
	... 

The log indicated Curator's connection was lost and continuously retrying (ConnectionLoss means the ZooKeeper cluster was unreachable). This caused the application to block at startup. Investigation revealed several possible causes:

Our solution: We addressed each cause: started the ZooKeeper service and adjusted firewall rules to allow access to ZooKeeper's port (2181 by default). After restarting the microservice, Curator connected and the application started cleanly. To prevent recurrence, we improved startup scripts by adding health checks for dependent services before application deployment—for example, automatically detecting ZooKeeper status and prompting or delaying startup if it's not ready. We also tuned Curator's timeout and retry parameters to fail fast during connection issues rather than hanging indefinitely.

These two cases taught us that startup troubleshooting requires rapid log-based diagnosis combined with awareness of component configuration and runtime dependencies. Whether managing transaction managers or service registry clients, understanding how they work and what parameters they expose makes root-cause discovery and resolution much faster.

Kubernetes Standard Release Pipeline

Having addressed initial deployment and runtime issues, we introduced Kubernetes to restructure the release pipeline. Our goal was end-to-end automation from build through deployment to release, replacing ad-hoc manual steps with standardized workflows. Here is how we implemented K8s-based releases in practice:

FROM openjdk:8-jre-slim
COPY app-1.0.jar /app/app.jar
CMD ["java", "-jar", "/app/app.jar", "--spring.config.location=/app/config/"]

We also packaged configuration files needed at runtime into the image's /app/config/ directory (or mounted them via ConfigMap; see below). This ensured the container found the correct configuration on startup. After writing the Dockerfile, we built the image using an internal CI tool and pushed it to a private image registry (e.g., registry.example.com/myteam/app:1.0).

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp-container
        image: registry.example.com/myteam/app:1.0
        ports:
        - containerPort: 8080
        env:
        - name: JAVA_OPTS
          value: "-Xms512m -Xmx512m"
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10

This manifest declares a two-replica deployment and configures containers to use the image we built. It also sets readiness and liveness probes that periodically hit the application's /health endpoint. These probes ensure the application only receives traffic when healthy and automatically restart containers on failure, improving release reliability.

Kubernetes standardized our release process into a pipeline workflow, enabling one-click deployments and rollbacks while dramatically reducing human error. Every deployment is logged and monitored, making issue tracking and rapid recovery far more straightforward.

Logging and Monitoring Infrastructure

As the system evolved toward containers and distributed architecture, we built a comprehensive logging and monitoring stack for operational troubleshooting and performance tuning.

Through this logging and monitoring infrastructure, we dramatically improved system observability. We moved from guesswork during outages to data-driven troubleshooting. Not only did mean time to recovery (MTTR) drop, but routine performance optimization became evidence-based. Operations became far less reactive.

Impact Assessment: Before and After

Comparing results across the improvements:

Area Before (Manual & Offline) After (Automated Pipeline & K8s)
Deployment Manual JAR transfer and startup scripts; long and error-prone release cycles Standardized container images, automated CI/CD build-to-release; fast and reproducible
Release reliability Ad-hoc processes, manual rollback on error; configuration inconsistency across servers Kubernetes rolling updates, seamless zero-downtime releases, automatic rollback on failure; consistent configuration across environment
Large-scale data operations Manual export of large tables was time-consuming and failure-prone Tooling with batch export improves efficiency several-fold; data migration is more predictable
Incident diagnosis Logs scattered across servers, problem identification took hours Centralized log search and real-time monitoring alerts; most issues detected and located within minutes
System visibility Relied on manual observation with no advance warning Comprehensive dashboards and alerting; problems surface before they impact users

The improvements are significant. Release efficiency improved dramatically—from half-hour manual deployments requiring multiple people to pipeline-driven releases typically completed in minutes with minimal intervention. Incident diagnosis improved even more: what once took 1–2 hours of concentrated log analysis now surfaces in minutes via centralized logging and monitoring alerts. Overall, these practices transformed our team's DevOps model and provided the stability needed for rapid business iteration.

Lessons and Future Directions

Through this journey from offline deployment to Kubernetes pipelines, our team gained valuable experience and confirmed the value of modern infrastructure practices. Here are the key takeaways and future directions:

Finally, we hope this practical summary proves useful to readers. Infrastructure modernization is gradual. From early experimentation with offline deployment to cloud-native practices today, each step brought both challenges and learning. As hands-on engineers, we should embrace the changes modern tools bring while staying alert to subtle issues, building experience, and continuously improving stability and delivery efficiency. We look forward to sharing more practical insights with the community as we continue.

© 2026 Yuxu Ge ·